package il.ac.tau.cvws.scrabblesidekick.server.util.cv;

import il.ac.tau.cvws.scrabblesidekick.server.constant.FileConstants;
import il.ac.tau.cvws.scrabblesidekick.server.util.debug.DebugUtil;
import il.ac.tau.cvws.scrabblesidekick.server.util.debug.DebugUtil.DebugTypes;
import il.ac.tau.cvws.scrabblesidekick.shared.util.math.Point;

import java.awt.image.BufferedImage;
import java.awt.image.LookupOp;
import java.awt.image.ShortLookupTable;
import java.io.File;
import java.util.Collection;
import java.util.Random;

import com.googlecode.javacv.cpp.opencv_core;
import com.googlecode.javacv.cpp.opencv_core.CvMat;
import com.googlecode.javacv.cpp.opencv_core.CvRect;
import com.googlecode.javacv.cpp.opencv_core.CvScalar;
import com.googlecode.javacv.cpp.opencv_core.CvSize;
import com.googlecode.javacv.cpp.opencv_core.IplImage;
import com.googlecode.javacv.cpp.opencv_highgui;
import com.googlecode.javacv.cpp.opencv_imgproc;
import com.googlecode.javacv.cpp.opencv_imgproc.IplConvKernel;

/* Various utility functions for use with openCV.
 * Most of these methods are here in order to wrap openCV with a nicer
 * object-oriented API, and some are here to group common batch operations.
 */
public class CvUtil
{
	public static void drawCircle(IplImage image, int x, int y, int radius)
	{
		drawCircle(image, x, y, radius, 255.0, 0.0, 0.0);
	}
	
	public static void drawCircle(IplImage image, int x, int y, int radius, double r, double g, double b)
	{
		opencv_core.cvDrawCircle(image,
				opencv_core.cvPoint(x, y), radius,
				opencv_core.CV_RGB(r, g, b),
				2, 8, 0);
	}
	
	public static void drawRect(IplImage image, int x, int y, int width, int height)
	{
		opencv_core.cvDrawRect(image,
				opencv_core.cvPoint(x, y),
				opencv_core.cvPoint(x + width, y + height),
				opencv_core.CV_RGB(255.0, 0.0, 0.0),
				1, 8, 0);
	}
	
	public static void drawLine(IplImage image, int x1, int y1, int x2, int y2)
	{
		drawLine(image, x1, y1, x2, y2, 255.0, 0.0, 0.0);
	}
	
	public static void drawLine(IplImage image, int x1, int y1, int x2, int y2, double r, double g, double b)
	{
		opencv_core.cvDrawLine(image,
				opencv_core.cvPoint(x1, y1),
				opencv_core.cvPoint(x2, y2),
				opencv_core.CV_RGB(r, g, b),
				1, 8, 0);
	}
	
	public static IplImage drawPoints(IplImage srcImage, Collection<Point> points)
	{
		IplImage resultImage = srcImage.clone();
		
		int r = new Random().nextInt(256);
		int g = new Random().nextInt(256);
		int b = new Random().nextInt(256);
		
		for (Point p : points)
		{
			opencv_core.cvCircle(resultImage,
					opencv_core.cvPoint((int)p.getX(), (int)p.getY()), 10,
					opencv_core.CV_RGB(r, g, b),
					10, 8, 0);	
		}
		
		return resultImage;
	}
	
	public static IplImage loadImage(String subPath)
	{
		return loadImage(subPath, false);
	}
	
	public static IplImage loadImage(String subPath, boolean isGrayscale)
	{
		String path = FileConstants.WORKING_DIR + subPath;
		
		IplImage nativeImage = null;
		IplImage resultImage = null;
		
		if (isGrayscale)
		{
			nativeImage = opencv_highgui.cvLoadImage(path, opencv_highgui.CV_LOAD_IMAGE_GRAYSCALE);
		}
		else
		{
			nativeImage = opencv_highgui.cvLoadImage(path);
		}
		
		resultImage = nativeImage.clone();
		
		opencv_core.cvReleaseImage(nativeImage);
		
		return resultImage;
	}
	
	public static void saveImage(IplImage image, String subPath)
	{
		String targetPath = FileConstants.WORKING_DIR + subPath;
		
		File targetFile = new File(targetPath);
		File targetDir = targetFile.getParentFile();
		
		if (!targetDir.exists())
		{
			targetDir.mkdirs();
		}
		
		opencv_highgui.cvSaveImage(targetPath, image);
	}
	
	public static IplImage invertImage(IplImage srcImage)
	{
		short[] col = new short[256];
		
		for (int i = 0; i < 256; i++) 
		{
		    col[i] = (short)(255 - i);
		}
		
		short[][] data = {col};
		
		LookupOp op = new LookupOp(new ShortLookupTable(0, data), null);
		BufferedImage targetBufImage = op.filter(srcImage.getBufferedImage(), null);
		
		return IplImage.createFrom(targetBufImage);
	}
	
	public static IplImage toGrayscaleImage(IplImage image)
	{
		IplImage grayImage = IplImage.create(image.width(), image.height(), image.depth(), 1);
		opencv_imgproc.cvCvtColor(image, grayImage, opencv_imgproc.CV_BGR2GRAY);
		
		return grayImage;
	}
	
	public static IplImage warpImagePerspective(IplImage image, int targetWidth, int targetHeight, CvMat mat)
	{
		IplImage resultImage = IplImage.create(targetWidth, targetHeight, image.depth(), image.nChannels());
		opencv_imgproc.cvWarpPerspective(image, resultImage, mat);
		
		return resultImage;
	}
	
	public static IplImage warpImageAffine(IplImage image, int targetWidth, int targetHeight, CvMat mat)
	{
		IplImage resultImage = IplImage.create(targetWidth, targetHeight, image.depth(), image.nChannels());
		opencv_imgproc.cvWarpAffine(image, resultImage, mat);
		
		return resultImage;
	}
	
	public static IplImage cropImage(IplImage image, int x, int y, int width, int height)
	{
		return cropImage(image, new CvRect(x, y, width, height));
	}
	
	public static IplImage cropImage(IplImage image, CvRect rect)
	{ 
		IplImage resImage = IplImage.create(rect.width(), rect.height(), image.depth(), image.nChannels()); 
		
		opencv_core.cvSetImageROI(image, rect);
		opencv_core.cvCopy(image, resImage); 
		opencv_core.cvResetImageROI(image); 
		
		return resImage;
	}
	
	public static IplImage resizeImage(IplImage image, double factor)
	{
		int newWidth = (int)(factor * image.width());
		int newHeight = (int)(factor * image.height());
		
		return resizeImage(image, newWidth, newHeight);
	}
	
	public static IplImage resizeImage(IplImage image, int newWidth, int newHeight)
	{
		IplImage newImage = IplImage.create(newWidth, newHeight, image.depth(), image.nChannels());
		opencv_imgproc.cvResize(image, newImage);
		
		return newImage;
	}
		
	public static IplImage erodeImage(IplImage image, int kernelSize)
	{
		IplImage erodedImage = IplImage.createCompatible(image);
		opencv_imgproc.cvErode(image, erodedImage,
				IplConvKernel.create(2 * kernelSize + 1, 2 * kernelSize + 1,
				kernelSize, kernelSize, opencv_imgproc.MORPH_ERODE, null), 1);
		
		return erodedImage;
	}
	
	public static IplImage dilateImage(IplImage image, int kernelSize)
	{
		IplImage dilatedImage = IplImage.createCompatible(image);
		opencv_imgproc.cvDilate(image, dilatedImage,
				IplConvKernel.create(2 * kernelSize + 1, 2 * kernelSize + 1,
				kernelSize, kernelSize, opencv_imgproc.MORPH_DILATE, null), 1);
		
		return dilatedImage;
	}
	
	public static IplImage blurImage(IplImage image, int kernelSize)
	{
		IplImage blurredImage = IplImage.createCompatible(image);
		opencv_imgproc.GaussianBlur(image, blurredImage,
				new CvSize(kernelSize, kernelSize), 0.0, 0.0, opencv_imgproc.BORDER_DEFAULT);
		
		return blurredImage;
	}
	
	public static IplImage sharpenImage(IplImage image, int kernelSize)
	{
		IplImage blurredImage = blurImage(image, kernelSize);
		
		IplImage sharpImage = IplImage.createCompatible(image);
		opencv_core.cvAddWeighted(image, 1.5, blurredImage, -0.5, 0.0, sharpImage);
		
		return sharpImage;
	}
	
	public static IplImage contrastImage(IplImage srcImage, double threshold, double factor)
	{
		short[] contrast = new short[256];
		int absThreshold = (int)(255 * threshold);
		
		if (factor != 0.0)
		{
		    for (int i = 0; i < 256; i++) 
		    {
				if (i > absThreshold) 
				{
					contrast[i] = (short)(i + ((i - absThreshold) * factor));
				}
				else if (i < absThreshold + 1)
				{
					contrast[i] = (short)(i - (((absThreshold + 1) - i) * factor));
				}
				    
				if (contrast[i] > 255) 
				{
					contrast[i] = 255;
				}
				else if (contrast[i] < 0)
				{
					contrast[i] = 0;
				}
		    }
		}
		
		short[][] data = { contrast, contrast, contrast };
		
		LookupOp op = new LookupOp(new ShortLookupTable(0, data), null);
		BufferedImage targetBufImage = op.filter(srcImage.getBufferedImage(), srcImage.getBufferedImage());
		
		return IplImage.createFrom(targetBufImage);
	}
	
	public static CvRect findCleanImageFrame(IplImage image,
			int minFrameWidth, int maxFrameWidth,
			int minFrameHeight, int maxFrameHeight, int frameThickness)
	{
		int width = image.width();
		int height = image.height();
		
		int widthDelta = maxFrameWidth - minFrameWidth;
		int heightDelta = maxFrameHeight - minFrameHeight;
		
		int stepCount = Math.max(widthDelta, heightDelta);
		
		double widthStep = widthDelta * (1.0 / stepCount);
		double heightStep = heightDelta * (1.0 / stepCount);
		
		for (int step = stepCount; step >= 0; step--)
		{
			int frameWidth = (int)(minFrameWidth + (step * widthStep));
			int frameHeight = (int)(minFrameHeight + (step * heightStep));
			
			int maxIterX = width - frameWidth;
			int maxIterY = height - frameHeight;
			
			for (int i = 0; i < maxIterY; i++)
			{
				int iy = ((maxIterY - 1) / 2) + ((i + 1) / 2) * ((i % 2 == 0) ? -1 : 1);
				
				for (int j = 0; j < maxIterX; j++)
				{
					int ix = ((maxIterX - 1) / 2) + ((j + 1) / 2) * ((j % 2 == 0) ? -1 : 1);
					
					boolean isClean = true;
					
					for (int t = 0; t < frameThickness; t++)
					{
						int fx = ix + t;
						int fy = iy + t;
						
						int fw = frameWidth - 2 * t;
						int fh = frameHeight - 2 * t;
						
						for (int x = fx; x < fx + fw; x++)
						{
							CvScalar top = opencv_core.cvGet2D(image, fy, x);
							
							if (top.magnitude() == 0.0)
							{
								isClean = false;
								break;
							}
							
							CvScalar bottom = opencv_core.cvGet2D(image, fy + fh, x);
							
							if (bottom.magnitude() == 0.0)
							{
								isClean = false;
								break;
							}
						}
						
						if (!isClean)
						{
							break;
						}
						
						for (int y = fy; y < fy + fh; y++)
						{
							CvScalar left = opencv_core.cvGet2D(image, y, fx);
							
							if (left.magnitude() == 0.0)
							{
								isClean = false;
								break;
							}
							
							CvScalar right = opencv_core.cvGet2D(image, y, fx + fw);
							
							if (right.magnitude() == 0.0)
							{
								isClean = false;
								break;
							}
						}
					
						if (!isClean)
						{
							break;
						}
					}
					
					if (!isClean)
					{
						continue;
					}
					
					int x1 = ix;
					int y1 = iy;
					int x2 = ix + frameWidth;
					int y2 = iy + frameHeight;
					
					for (int x = ix; x < ix + frameWidth; x++)
					{
						boolean isSideClean = true;
						
						for (int y = iy; y < iy + frameHeight; y++)
						{
							CvScalar value = opencv_core.cvGet2D(image, y, x);
							
							if (value.magnitude() == 0.0)
							{
								isSideClean = false;
								break;
							}
						}
						
						if (!isSideClean)
						{
							break;
						}
						
						x1 = x;
					}
					
					for (int x = ix + frameWidth - 1; x >= ix; x--)
					{
						boolean isSideClean = true;
						
						for (int y = iy; y < iy + frameHeight; y++)
						{
							CvScalar value = opencv_core.cvGet2D(image, y, x);
							
							if (value.magnitude() == 0.0)
							{
								isSideClean = false;
								break;
							}
						}
						
						if (!isSideClean)
						{
							break;
						}
						
						x2 = x;
					}
					
					for (int y = iy; y < iy + frameHeight; y++)
					{
						boolean isSideClean = true;
						
						for (int x = ix; x < ix + frameWidth; x++)
						{
							CvScalar value = opencv_core.cvGet2D(image, y, x);
							
							if (value.magnitude() == 0.0)
							{
								isSideClean = false;
								break;
							}
						}
						
						if (!isSideClean)
						{
							break;
						}
						
						y1 = y;
					}
					
					for (int y = iy + frameHeight - 1; y >= iy; y--)
					{
						boolean isSideClean = true;
						
						for (int x = ix; x < ix + frameWidth; x++)
						{
							CvScalar value = opencv_core.cvGet2D(image, y, x);
							
							if (value.magnitude() == 0.0)
							{
								isSideClean = false;
								break;
							}
						}
						
						if (!isSideClean)
						{
							break;
						}
						
						y2 = y;
					}
					
					CvRect resultRect = opencv_core.cvRect(
							x1 + 1,
							y1 + 1,
							x2 - x1 - 1,
							y2 - y1 - 1);
					
					return resultRect;
				}
			}
		}
		
		return null;
	}
	
	public static IplImage createThresholdImage(IplImage image, double threshold)
	{
		return createThresholdImage(image, threshold, true, false);
	}
	
	public static IplImage createThresholdImage(IplImage image, double threshold,
			boolean preSharpen, boolean retainChannels)
	{
		IplImage sharpImage  = (preSharpen ? CvUtil.sharpenImage(image, 5) : image);
		IplImage grayImage = CvUtil.toGrayscaleImage(sharpImage);
		
		IplImage thresholdImage = IplImage.createCompatible(grayImage);
		opencv_imgproc.cvThreshold(grayImage, thresholdImage,
				(int)(255 * threshold), 255, opencv_imgproc.CV_THRESH_BINARY);
		
		DebugUtil.saveImage(grayImage, DebugTypes.LAYOUT, "gray_image", true);
		DebugUtil.saveImage(thresholdImage, DebugTypes.LAYOUT, "threshold", true);
		
		if (retainChannels)
		{
			IplImage compatibleImage = IplImage.createCompatible(image);
			opencv_imgproc.cvCvtColor(thresholdImage, compatibleImage, opencv_imgproc.CV_GRAY2BGR);
			
			DebugUtil.saveImage(thresholdImage, DebugTypes.LAYOUT, "compatible", true);
			
			return compatibleImage;
		}
		else
		{
			return thresholdImage;
		}
	}
}
